package gateway

import (
	"bytes"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha1"
	"encoding/hex"
	"fmt"
	"io"
	"io/ioutil"
	"strings"
	"time"

	"gitee.com/zh-jn/harmonica-previewer-online/api"
	compilercli "gitee.com/zh-jn/harmonica-previewer-online/compiler/deveco-compiler/v1/client"
	"gitee.com/zh-jn/harmonica-previewer-online/previewer/harmonica/v1/manager"
	managercli "gitee.com/zh-jn/harmonica-previewer-online/previewer/harmonica/v1/manager/client"
	sidecarcli "gitee.com/zh-jn/harmonica-previewer-online/previewer/harmonica/v1/sidecar/client"
	storagecli "gitee.com/zh-jn/harmonica-previewer-online/storage/localstorage/client"
)

type PreviewerCgiConfig struct {
	ManagerAddr string
	ManagerPort int

	CompilerAddr string
	CompilerPort int

	StorageAddr string
	StoragePort int

	SidecarPort int

	SessionKey string // 64 bytes
}

const SessionKeySize = 64

func NewPreviewerCgi(cfg PreviewerCgiConfig) *PreviewerCgi {
	mcli := managercli.NewManagerClient(managercli.ClientConfig{
		Addr: cfg.ManagerAddr,
		Port: cfg.ManagerPort,
	})

	ccli := compilercli.NewCompileServiceClient(
		cfg.CompilerAddr,
		cfg.CompilerPort,
	)

	shareCli := storagecli.NewLocalFileStorageClient(
		cfg.StorageAddr,
		cfg.StoragePort,
		"share",
	)
	jsBundleCli := storagecli.NewLocalFileStorageClient(
		cfg.StorageAddr,
		cfg.StoragePort,
		"jsbundle",
	)

	return &PreviewerCgi{
		managerCli:         mcli,
		compilerCli:        ccli,
		shareStorageCli:    shareCli,
		jsBundleStorageCli: jsBundleCli,
		sidecarPort:        cfg.SidecarPort,
		sessionKey:         cfg.SessionKey,
	}
}

type PreviewerCgi struct {
	managerCli  *managercli.ManagerClient
	compilerCli api.Compiler

	shareStorageCli    api.Storage
	jsBundleStorageCli api.Storage

	sidecarPort int

	sessionKey string
}

func getKeyByte(sessionKey string) []byte {
	key := make([]byte, SessionKeySize)
	_okey := []byte(sessionKey)
	if len(_okey) >= SessionKeySize {
		key = _okey[:SessionKeySize]
	} else {
		padding := make([]byte, SessionKeySize-len(_okey))
		key = append(_okey, padding...)
	}
	return key
}

func (p *PreviewerCgi) GetSessionId() (string, error) {
	rb := make([]byte, 8)
	_, err := rand.Read(rb)
	if err != nil {
		return "", err
	}
	key := getKeyByte(p.sessionKey)

	hm := hmac.New(sha1.New, key)
	_, err = hm.Write(rb)
	if err != nil {
		return "", err
	}

	res := hm.Sum(nil)
	res = append(rb, res[:8]...)

	return hex.EncodeToString(res), nil
}

func checkSessionId(sessionKey, sessionId string) (bool, error) {
	sbytes, err := hex.DecodeString(sessionId)
	if err != nil {
		return false, err
	}
	if len(sbytes) != 16 {
		return false, fmt.Errorf("length is not 16")
	}
	rb := sbytes[:8]
	key := getKeyByte(sessionKey)

	hm := hmac.New(sha1.New, key)
	_, err = hm.Write(rb)
	if err != nil {
		return false, err
	}

	res := hm.Sum(nil)
	res = append(rb, res[:8]...)

	checkString := hex.EncodeToString(res)

	if checkString != sessionId {
		return false, nil
	}

	return true, nil
}

func (p *PreviewerCgi) ShareCode(sessionId string, codeBundle io.Reader) (shareId string, e error) {
	if ok, err := checkSessionId(p.sessionKey, sessionId); !ok {
		e = fmt.Errorf("invalid session id, %v", err)
		return
	}

	codebuf, err := ioutil.ReadAll(codeBundle)
	if err != nil {
		e = err
		return
	}
	h := sha1.New()
	if _, err = h.Write(codebuf); err != nil {
		e = err
		return
	}
	shareId = hex.EncodeToString(h.Sum(nil))

	e = p.shareStorageCli.Put(api.StorageKey(shareId), bytes.NewReader(codebuf))

	return
}

func (p *PreviewerCgi) CompileCode(sessionId string, codeBundle io.Reader) (jsBundleId string, e error) {
	if ok, err := checkSessionId(p.sessionKey, sessionId); !ok {
		e = fmt.Errorf("invalid session id, %v", err)
		return
	}
	codebuf, err := ioutil.ReadAll(codeBundle)
	if err != nil {
		e = err
		return
	}
	h := sha1.New()
	if _, err = h.Write(codebuf); err != nil {
		e = err
		return
	}
	jsBundleId = hex.EncodeToString(h.Sum(nil))

	exists, err := p.jsBundleStorageCli.Exists(api.StorageKey(jsBundleId))
	if err != nil {
		e = fmt.Errorf("check jsbundle[%v] exists err, %v", err)
		return
	}

	// 已经存在编译好的jsbundle 使用即可
	if exists {
		return
	}

	// 编译
	jsbuf, err := p.compilerCli.Compile(codebuf)
	if err != nil {
		e = fmt.Errorf("compile js bundle err, %v", err)
		return
	}

	// 保存jsbundle
	if e = p.jsBundleStorageCli.Put(api.StorageKey(jsBundleId), bytes.NewReader(jsbuf)); e != nil {
		e = fmt.Errorf("save jsbundle err, %v", e)
		return
	}

	return
}

func (p *PreviewerCgi) ReleasePreviewer(sessionId, previewerId string) (e error) {
	if ok, err := checkSessionId(p.sessionKey, sessionId); !ok {
		e = fmt.Errorf("invalid session id, %v", err)
		return
	}

	return p.managerCli.ReleasePreviewer(sessionId, previewerId)
}

func (p *PreviewerCgi) LaunchPreviewer(sessionId, jsBundleId string) (previewerD manager.PreviewerDescriptor, e error) {
	retmsg := []string{}
	start := time.Now().UnixNano()
	retmsg = append(retmsg, fmt.Sprintf("start: %v", start))

	// 分配previewer
	if ok, err := checkSessionId(p.sessionKey, sessionId); !ok {
		e = fmt.Errorf("invalid session id, %v", err)
		return
	}
	previewerD, e = p.managerCli.AllocatePreviewer(sessionId) // preveiwer 和 sessionId绑定，与jsbundle无关
	if e != nil {
		e = fmt.Errorf("allocate previewer err, %v", e)
		return
	}

	retmsg = append(retmsg, fmt.Sprintf("allocate: %v", time.Now().UnixNano()-start))

	jsBundle, err := p.jsBundleStorageCli.Get(api.StorageKey(jsBundleId))
	if err != nil {
		e = fmt.Errorf("[%v] get jsbundle err, %v", previewerD.PreviewerId, err)
		return
	}

	retmsg = append(retmsg, fmt.Sprintf("getJsbundle: %v", time.Now().UnixNano()-start))

	scli := sidecarcli.NewSideCarClient(fmt.Sprintf("%v.%v", previewerD.PreviewerId, previewerD.Namespace), p.sidecarPort)

	if e = scli.SetJsBundle(jsBundleId, sessionId, jsBundle); e != nil {
		e = fmt.Errorf("[%v] set jsbundle err, %v", previewerD.PreviewerId, e)
		return
	}

	retmsg = append(retmsg, fmt.Sprintf("setJsBundle: %v", time.Now().UnixNano()-start))

	fmt.Printf("launch previewer profile: %v\n", strings.Join(retmsg, ","))
	return
}
